﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Dynamic;
using System.Reflection;
using System.Reflection.Emit;

namespace NerdOnRails.DynamicProxy
{
	 public class ProxyObject<T> : System.Dynamic.DynamicObject where T : class, new()
	 {
		  // Properties
		  #region TypeOfInvokableObject
		  private Type TypeOfInvokableObject
		  {
				get;
				set;
		  }
		  #endregion

		  #region InvocationHandler
		  private IInvocation InvocationHandler
		  {
				get;
				set;
		  }
		  #endregion

		  #region InvokableObject
		  private T InvokableObject
		  {
				get;
				set;
		  }
		  #endregion

		  // Constructors
		  public ProxyObject() { }

		  internal ProxyObject(IInvocation invocationHandler, params Object[] args)
		  {
				Type type = typeof(T);
				Type[] parameterTypes = null;
				ConstructorInfo constructor = type.GetConstructors().First(constr => constr.GetParameters().Select(parameter => parameter.GetType()).SequenceEqual(parameterTypes));
				this.InvokableObject = constructor.Invoke(args) as T;

				this.InvocationHandler = invocationHandler;
				this.TypeOfInvokableObject = typeof(T);
		  }

		  internal ProxyObject(IInvocation invocationHandler)
		  {
				this.InvokableObject = new T();
				this.InvocationHandler = invocationHandler;
				this.TypeOfInvokableObject = typeof(T);

		  }

		  // Methods

		  #region TryInvokeMember
		  public override bool TryInvokeMember(InvokeMemberBinder binder, Object[] args, out Object result)
		  {
				Boolean operationResult = false;
				Type[] parameterTypes = args.Select(arg => arg.GetType()).ToArray();
				var method = this.TypeOfInvokableObject.GetMethod(binder.Name, BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static, null, parameterTypes, null);

				if (method != null)
				{
					 this.InvocationHandler.BeforeMethodInvocation(binder.Name, this.TypeOfInvokableObject, args);
					 result = method.Invoke(this.InvokableObject, args);
					 this.InvocationHandler.AfterMethodInvocation(binder.Name, this.TypeOfInvokableObject, result);
					 operationResult = true;
				}
				else
				{
					 operationResult = base.TryInvokeMember(binder, args, out result);
				}
				return operationResult;
		  }
		  #endregion

		  #region TryGetMember
		  public override bool TryGetMember(GetMemberBinder binder, out Object result)
		  {
				Boolean operationResult = false;
				var propertyQuery = this.LoadProperty(binder.Name);
				PropertyInfo propertyToFill = propertyQuery.FirstOrDefault();
				operationResult = propertyToFill != null;
				result = propertyToFill.GetValue(this.InvokableObject, null);

				return operationResult;
		  }
		  #endregion

		  #region GetParameterTypes
		  private IEnumerable<Type> GetParameterTypes(params Object[] args)
		  {
				return from current in args select this.GetActualType(current);
		  }
		  #endregion

		  #region GetActualType
		  private Type GetActualType(Object obj)
		  {
				return (obj is ParameterInfo) ? (obj as ParameterInfo).ParameterType : obj.GetType();
		  }

		  #endregion

		  #region ToString
		  public override string ToString()
		  {
				return String.Concat("ProxyObject-", Guid.NewGuid().ToString());
		  }
		  #endregion

		  #region TrySetMember
		  public override bool TrySetMember(SetMemberBinder binder, Object value)
		  {
				Boolean result = false;
				var propertyQuery = this.LoadProperty(binder.Name);
				PropertyInfo propertyToFill = propertyQuery.FirstOrDefault();
				result = propertyToFill != null;
				if (result)
				{
					 propertyToFill.SetValue(this.InvokableObject, value, null);
				}
				return result;
		  }
		  #endregion

		  #region LoadProperty
		  private IEnumerable<PropertyInfo> LoadProperty(String binderName)
		  {
				var propertyQuery = from current in this.TypeOfInvokableObject.GetProperties()
										  where current.Name == binderName
										  select current;
				return propertyQuery;
		  }
		  #endregion

		  public override DynamicMetaObject GetMetaObject(System.Linq.Expressions.Expression parameter)
		  {
				DynamicMetaObject obj = base.GetMetaObject(parameter);

				return obj;
		  }

		  public override bool TryConvert(ConvertBinder binder, out object result)
		  {

				return base.TryConvert(binder, out result);
		  }
	 }
}
